<!DOCTYPE html>
<html lang="zh-CN">





<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/blog/img/favicon.png">
  <link rel="icon" type="image/png" href="/blog/img/favicon.png">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  
    <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
  
  <meta name="theme-color" content="#35495e">
  <meta name="description" content="小菜头的博客，主要记录学习笔记，包含：html,css,javascript,jQuery,vue等">
  <meta name="author" content="A-cai">
  <meta name="keywords" content="html,css,javascript,jQuery,vue">
  <title>如何实现token加密 - A-cai的blog</title>

  <link  rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />


  <link  rel="stylesheet" href="https://cdn.staticfile.org/github-markdown-css/4.0.0/github-markdown.min.css" />
  <link  rel="stylesheet" href="/blog/lib/hint/hint.min.css" />

  
    <link  rel="stylesheet" href="https://cdn.staticfile.org/highlight.js/10.0.0/styles/atom-one-dark.min.css" />
  

  


<!-- 主题依赖的图标库，不要自行修改 -->

<link rel="stylesheet" href="//at.alicdn.com/t/font_1749284_yg9cfy8wd6.css">



<link rel="stylesheet" href="//at.alicdn.com/t/font_1736178_pjno9b9zyxs.css">


<link  rel="stylesheet" href="/blog/css/main.css" />

<!-- 自定义样式保持在最底部 -->


  <script  src="/blog/js/utils.js" ></script>
<meta name="generator" content="Hexo 4.2.0"></head>


<body>
  <header style="height: 70vh;">
    <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">
    <a class="navbar-brand"
       href="/blog/">&nbsp;<strong>A-cai的学习笔记</strong>&nbsp;</a>

    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/blog/">
                <i class="iconfont icon-home-fill"></i>
                首页
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/blog/archives/">
                <i class="iconfont icon-archive-fill"></i>
                归档
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/blog/categories/">
                <i class="iconfont icon-category-fill"></i>
                分类
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/blog/tags/">
                <i class="iconfont icon-tags-fill"></i>
                标签
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/blog/about/">
                <i class="iconfont icon-user-fill"></i>
                关于
              </a>
            </li>
          
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" data-toggle="modal" data-target="#modalSearch">&nbsp;&nbsp;<i
                class="iconfont icon-search"></i>&nbsp;&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>
</nav>

    <div class="banner intro-2" id="background" parallax=true
         style="background: url('/blog/img/default.png') no-repeat center center;
           background-size: cover;">
      <div class="full-bg-img">
        <div class="mask flex-center" style="background-color: rgba(0, 0, 0, 0.3)">
          <div class="container text-center white-text fade-in-up">
            <span class="h2" id="subtitle">
              
            </span>

            
              
  <div class="mt-3 post-meta">
    <i class="iconfont icon-date-fill" aria-hidden="true"></i>
    <time datetime="2020-06-26 14:04">
      2020年6月26日 下午
    </time>
  </div>


<div class="mt-1">
  
    
    <span class="post-meta mr-2">
      <i class="iconfont icon-chart"></i>
      4.1k 字
    </span>
  

  
    
    <span class="post-meta mr-2">
      <i class="iconfont icon-clock-fill"></i>
      
      
      46
       分钟
    </span>
  

  
  
    
      <!-- 不蒜子统计文章PV -->
      <span id="busuanzi_container_page_pv" style="display: none">
        <i class="iconfont icon-eye" aria-hidden="true"></i>
        <span id="busuanzi_value_page_pv"></span> 次
      </span>
    
  
</div>

            
          </div>

          
        </div>
      </div>
    </div>
  </header>

  <main>
    
      

<div class="container-fluid">
  <div class="row">
    <div class="d-none d-lg-block col-lg-2"></div>
    <div class="col-lg-8 nopadding-md">
      <div class="container nopadding-md" id="board-ctn">
        <div class="py-5" id="board">
          <div class="post-content mx-auto" id="post">
            
              <p class="note note-info">
                
                  本文最后更新于：2020年4月2日 下午
                
              </p>
            
            <article class="markdown-body">
              <h1 id="Token-服务端身份验证的流行方案"><a href="#Token-服务端身份验证的流行方案" class="headerlink" title="Token - 服务端身份验证的流行方案"></a>Token - 服务端身份验证的流行方案</h1><h3 id="简述："><a href="#简述：" class="headerlink" title="简述："></a>简述：</h3><ol>
<li>需要一个secret（随机数）</li>
<li>后端利用secret和加密算法(如：HMAC-SHA256)对payload(如账号密码)生成一个字符串(token)，返回前端</li>
<li>前端每次request在header中带上token</li>
<li>后端用同样的算法解密</li>
</ol>
<h1 id="身份认证"><a href="#身份认证" class="headerlink" title="身份认证"></a>身份认证</h1><hr>
<p>服务端提供资源给客户端，但是某些资源是<strong>有条件</strong>的。所以服务端要能够识别请求者的身份，然后再判断所请求的资源是否可以给请求者。</p>
<p>token是一种身份验证的机制，初始时用户提交账号数据给服务端，服务端采用一定的策略生成一个字符串（token），token字符串中包含了少量的用户信息，并且有一定的期限。服务端会把token字符串传给客户端，客户端保存token字符串，并在接下来的请求中带上这个字符串。</p>
<p>它的工作流程大概是这样：</p>
<p><img src="https:////upload-images.jianshu.io/upload_images/3170835-b5ff425dbf277189.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/652/format/webp" srcset="/blog/img/loading.gif" alt="img"></p>
<p>组件图</p>
<h1 id="Token机制"><a href="#Token机制" class="headerlink" title="Token机制"></a>Token机制</h1><p>在这样的流程下，我们需要考虑下面几个问题：</p>
<ol>
<li>服务端如何根据token获取用户的信息？</li>
<li>如何确保识别<strong>伪造</strong>的token？<br> 这里是指token不是经过服务端来生成的。</li>
<li>如何应付<strong>冒充</strong>的情况？<br> 非法客户端拦截了合法客户端的token，然后使用这个token向服务端发送请求，冒充合法客户端。</li>
</ol>
<h2 id="用户匹配"><a href="#用户匹配" class="headerlink" title="用户匹配"></a>用户匹配</h2><p>服务端在生成token时，加入少量的用户信息，比如用户的id。服务端接收到token之后，可以解析出这些数据，从而将token和用户关联了起来。</p>
<h2 id="防伪造"><a href="#防伪造" class="headerlink" title="防伪造"></a>防伪造</h2><p>一般情况下，建议放入token的数据是不敏感的数据，这样只要服务端使用私钥对数据生成签名，然后和数据拼接起来，作为token的一部分即可。</p>
<h2 id="防冒充"><a href="#防冒充" class="headerlink" title="防冒充"></a>防冒充</h2><h3 id="加干扰码"><a href="#加干扰码" class="headerlink" title="加干扰码"></a>加干扰码</h3><p>服务端在生成token时，使用了客户端的UA作为干扰码对数据加密，客户端进行请求时，会同时传入token、UA，服务端使用UA对token解密，从而验证用户的身份。</p>
<p>如果只是把token拷贝到另一个客户端使用，不同的UA会导致在服务端解析token失败，从而实现了一定程度的防冒充。但是攻击者如果猜到服务端使用UA作为加密钥，他可以修改自己的UA。</p>
<h3 id="有效期"><a href="#有效期" class="headerlink" title="有效期"></a>有效期</h3><p>给token加上有效期，即使被冒充也只是在一定的时间段内有效。这不是完美的防御措施，只是尽量减少损失。</p>
<p>服务端在生成token时，加入有效期。每次服务端接收到请求，解析token之后，判断是否已过期，如果过期就拒绝服务。</p>
<h4 id="token刷新"><a href="#token刷新" class="headerlink" title="token刷新"></a>token刷新</h4><p>如果token过期了，客户端应该对token续期或者重新生成token。这取决于token的过期机制。</p>
<ol>
<li>服务器缓存token及对应的过期时间<br> 这个时候就可以采用续期的方式，服务器更新过期时间，token再次有效。</li>
<li>token中含有过期时间<br> 这个时候需要重新生成token。</li>
</ol>
<p>在token续期或者重新生成token的时候，需要额外加入数据来验证身份。因为token已经过期了，即token已经不能用来验证用户的身份了。这个时候可以请求用户重新输入账号和密码，但是用户体验稍差。</p>
<p>另一种方式是使用摘要。服务端生成token，同时生成token的摘要，然后一起返回给客户端。客户端保存摘要，一般请求只需要用到token，在刷新token时，才需要用到摘要。服务端验证摘要，来验证用户的身份。因为摘要不会频繁的在客户端和服务端之间传输，所以被截取的概率较小。</p>
<h1 id="Token工作流程"><a href="#Token工作流程" class="headerlink" title="Token工作流程"></a>Token工作流程</h1><hr>
<h2 id="生成token"><a href="#生成token" class="headerlink" title="生成token"></a>生成token</h2><p><img src="https:////upload-images.jianshu.io/upload_images/3170835-60d815fbd936577a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/432/format/webp" srcset="/blog/img/loading.gif" alt="img"></p>
<p>生成token</p>
<p>一般在登录的时候生成token。Token管理者负责根据用户的数据生成token和摘要，摘要用来给APP端刷新token用，类似于<a href="https://link.jianshu.com?t=https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419317853&lang=zh_CN" target="_blank" rel="noopener">微信登录</a>中的refresh_token。</p>
<p>生成token的过程中，ua的充作干扰码。没有相同的ua，就无法解析生成的token字符串。如果客户端是混合开发的模式，生成token和使用token的代理可能不同（比如一个是h5，一个是原生），ua就会不同，token就无法成功的使用。可以选择其他的客户端数据作为干扰码，注意考虑下面的问题：</p>
<ol>
<li>不同的客户端，干扰码应该不同<br> 干扰码的很大一个作用是防冒充，如果选择的充当干扰码的客户端数据没有区分性，就达不到效果。</li>
<li>选择充当干扰码的数据，在哪些情况下会变化？这些情况是否合理？<br> 比如干扰码数据中含有app的版本号，那么app版本升级就会导致干扰码变化。服务端根据新的干扰码，无法解析旧的token，此时用户必须重新登录。这种情况是合理的吗？如果不合理，干扰码中就不应该含有app的版本号。</li>
</ol>
<h2 id="拦截验证"><a href="#拦截验证" class="headerlink" title="拦截验证"></a>拦截验证</h2><p><img src="https:////upload-images.jianshu.io/upload_images/3170835-1a2f9f20e337cf30.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/581/format/webp" srcset="/blog/img/loading.gif" alt="img"></p>
<p>拦截验证</p>
<p>客户端的每一次请求，都必须携带token、ua，拦截器会对敏感资源的访问进行拦截，然后根据ua解析token，解析不成功，表示token和ua不匹配。解析成功之后，判断token是否已过期，如果是，拒绝服务。所有都ok的情况下，拦截器放行，请求传达到业务服务者。</p>
<h2 id="token刷新-1"><a href="#token刷新-1" class="headerlink" title="token刷新"></a>token刷新</h2><p><img src="https:////upload-images.jianshu.io/upload_images/3170835-927164e92bde2055.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/464/format/webp" srcset="/blog/img/loading.gif" alt="img"></p>
<p>token刷新</p>
<p>当token过期，用户需要刷新token。刷新token本质上是这样的：</p>
<blockquote>
<p>服务端：这个token是你的吗？<br> 客户端：是的。<br> 服务端：当初我给你token的时候，还给了一个摘要，你把摘要拿过来证明。</p>
</blockquote>
<p>客户端需要把token、摘要、ua都传给服务端，服务端对token重新生成摘要，通过判断两个摘要是否相同来验证<strong>这次请求刷新token的客户端，是不是上次请求生成token的客户端</strong>。验证通过，服务端需要使用用户数据重新生成token，用户数据则来自用ua解析token的结果。</p>
<h1 id="基于-Token-的身份验证：JSON-Web-Token"><a href="#基于-Token-的身份验证：JSON-Web-Token" class="headerlink" title="基于 Token 的身份验证：JSON Web Token"></a>基于 Token 的身份验证：JSON Web Token</h1><p>很多大型网站也都在用，比如 Facebook，Twitter，Google+，Github 等等，比起传统的身份验证方法，Token 扩展性更强，也更安全点，非常适合用在 Web 应用或者移动应用上。Token 的中文有人翻译成 “令牌”，我觉得挺好，意思就是，你拿着这个令牌，才能过一些关卡。</p>
<p>文章先介绍了一下传统身份验证与基于 JWT 身份验证的方法，再理解一下 JWT 的 Token 的组成部分（头部，数据，签名），最后我们会在一个 Node.js 项目上实施签发与验证 JWT 的功能。</p>
<h2 id="传统身份验证的方法"><a href="#传统身份验证的方法" class="headerlink" title="传统身份验证的方法"></a>传统身份验证的方法</h2><p>HTTP 是一种没有状态的协议，也就是它并不知道是谁是访问应用。这里我们把用户看成是客户端，客户端使用用户名还有密码通过了身份验证，不过下回这个客户端再发送请求时候，还得再验证一下。</p>
<p>解决的方法就是，当用户请求登录的时候，如果没有问题，我们在服务端生成一条记录，这个记录里可以说明一下登录的用户是谁，然后把这条记录的 ID 号发送给客户端，客户端收到以后把这个 ID 号存储在 Cookie 里，下次这个用户再向服务端发送请求的时候，可以带着这个 Cookie ，这样服务端会验证一个这个 Cookie 里的信息，看看能不能在服务端这里找到对应的记录，如果可以，说明用户已经通过了身份验证，就把用户请求的数据返回给客户端。</p>
<p>上面说的就是 Session，我们需要在服务端存储为登录的用户生成的 Session ，这些 Session 可能会存储在内存，磁盘，或者数据库里。我们可能需要在服务端定期的去清理过期的 Session 。</p>
<h2 id="基于-Token-的身份验证方法"><a href="#基于-Token-的身份验证方法" class="headerlink" title="基于 Token 的身份验证方法"></a>基于 Token 的身份验证方法</h2><p>使用基于 Token 的身份验证方法，在服务端不需要存储用户的登录记录。大概的流程是这样的：</p>
<ol>
<li>客户端使用用户名跟密码请求登录</li>
<li>服务端收到请求，去验证用户名与密码</li>
<li>验证成功后，服务端会签发一个 Token，再把这个 Token 发送给客户端</li>
<li>客户端收到 Token 以后可以把它存储起来，比如放在 Cookie 里或者 Local Storage 里</li>
<li>客户端每次向服务端请求资源的时候需要带着服务端签发的 Token</li>
<li>服务端收到请求，然后去验证客户端请求里面带着的 Token，如果验证成功，就向客户端返回请求的数据</li>
</ol>
<h2 id="JWT"><a href="#JWT" class="headerlink" title="JWT"></a>JWT</h2><p>实施 Token 验证的方法挺多的，还有一些标准方法，比如 JWT，读作：<em>jot</em> ，表示：JSON Web Tokens 。JWT 标准的 Token 有三个部分：</p>
<ul>
<li>header（头部）</li>
<li>payload（数据）</li>
<li>signature（签名）</li>
</ul>
<p>中间用点分隔开，并且都会使用 Base64 编码，所以真正的 Token 看起来像这样：</p>
<div class="hljs"><pre><code class="hljs css"><span class="hljs-selector-tag">eyJhbGciOiJIUzI1NiJ9</span><span class="hljs-selector-class">.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ</span><span class="hljs-selector-class">.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc</span></code></pre></div>

<h3 id="Header"><a href="#Header" class="headerlink" title="Header"></a>Header</h3><p>每个 JWT token 里面都有一个 header，也就是头部数据。里面包含了使用的算法，这个 JWT 是不是带签名的或者加密的。主要就是说明一下怎么处理这个 JWT token 。</p>
<p>头部里包含的东西可能会根据 JWT 的类型有所变化，比如一个加密的 JWT 里面要包含使用的加密的算法。唯一在头部里面要包含的是 <em>alg</em> 这个属性，如果是加密的 JWT，这个属性的值就是使用的签名或者解密用的算法。如果是未加密的 JWT，这个属性的值要设置成 <em>none</em>。</p>
<p>示例：</p>
<div class="hljs"><pre><code class="hljs json">&#123;
  <span class="hljs-attr">"alg"</span>: <span class="hljs-string">"HS256"</span>
&#125;</code></pre></div>

<p>意思是这个 JWT 用的算法是 HS256。上面的内容得用 <a href="https://tools.ietf.org/html/rfc4648#section-5" target="_blank" rel="noopener">base64url</a> 的形式编码一下，所以就变成这样：</p>
<div class="hljs"><pre><code class="hljs gcode">eyJhbGciOiJIUzI<span class="hljs-number">1</span><span class="hljs-symbol">NiJ9</span></code></pre></div>

<h3 id="Payload"><a href="#Payload" class="headerlink" title="Payload"></a>Payload</h3><p>Payload 里面是 Token 的具体内容，这些内容里面有一些是标准字段，你也可以添加其它需要的内容。下面是标准字段：</p>
<ul>
<li>iss：Issuer，发行者</li>
<li>sub：Subject，主题</li>
<li>aud：Audience，观众</li>
<li>exp：Expiration time，过期时间</li>
<li>nbf：Not before</li>
<li>iat：Issued at，发行时间</li>
<li>jti：JWT ID</li>
</ul>
<p>比如下面这个 Payload ，用到了 <em>iss</em> 发行人，还有 <em>exp</em> 过期时间这两个标准字段。另外还有两个自定义的字段，一个是 <em>name</em> ，还有一个是 <em>admin</em> 。</p>
<div class="hljs"><pre><code class="hljs json">&#123;
 <span class="hljs-attr">"iss"</span>: <span class="hljs-string">"ninghao.net"</span>,
 <span class="hljs-attr">"exp"</span>: <span class="hljs-string">"1438955445"</span>,
 <span class="hljs-attr">"name"</span>: <span class="hljs-string">"wanghao"</span>,
 <span class="hljs-attr">"admin"</span>: <span class="hljs-literal">true</span>
&#125;</code></pre></div>

<p>使用 base64url 编码以后就变成了这个样子：</p>
<div class="hljs"><pre><code class="hljs gcode">eyJpc<span class="hljs-number">3</span>MiOiJuaW<span class="hljs-number">5</span><span class="hljs-symbol">naGFvLm5</span>ldCIsImV<span class="hljs-number">4</span>cCI<span class="hljs-number">6</span>IjE<span class="hljs-number">0</span>Mz<span class="hljs-name">g5</span><span class="hljs-symbol">NTU0</span><span class="hljs-symbol">NDUiLCJuYW1</span>lIjoid<span class="hljs-number">2</span>FuZ<span class="hljs-number">2</span>hhbyIsImFkbWluIjp<span class="hljs-number">0</span>c<span class="hljs-symbol">nVlfQ</span></code></pre></div>

<h3 id="Signature"><a href="#Signature" class="headerlink" title="Signature"></a>Signature</h3><p>JWT 的最后一部分是 Signature ，这部分内容有三个部分，先是用 Base64 编码的 header.payload ，再用加密算法加密一下，加密的时候要放进去一个 Secret ，这个相当于是一个密码，这个密码秘密地存储在服务端。</p>
<ul>
<li>header</li>
<li>payload</li>
<li>secret</li>
</ul>
<div class="hljs"><pre><code class="hljs lisp">const encodedString = base64UrlEncode(<span class="hljs-name">header</span>) + <span class="hljs-string">"."</span> + base64UrlEncode(<span class="hljs-name">payload</span>)<span class="hljs-comment">; </span>
HMACSHA256(<span class="hljs-name">encodedString</span>, 'secret')<span class="hljs-comment">;</span></code></pre></div>

<p>处理完成以后看起来像这样：</p>
<div class="hljs"><pre><code class="hljs gcode">SwyHTEx_RQppr<span class="hljs-number">97</span><span class="hljs-name">g4</span>J<span class="hljs-number">5</span>lKXtabJecpejuef<span class="hljs-number">8</span>AqKYMAJc</code></pre></div>

<p>最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样：</p>
<div class="hljs"><pre><code class="hljs css"><span class="hljs-selector-tag">eyJhbGciOiJIUzI1NiJ9</span><span class="hljs-selector-class">.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ</span><span class="hljs-selector-class">.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc</span></code></pre></div>

<p>客户端收到这个 Token 以后把它存储下来，下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ，然后进行验证，通过以后就会返回给客户端想要的资源。</p>
<h2 id="签发与验证-JWT"><a href="#签发与验证-JWT" class="headerlink" title="签发与验证 JWT"></a>签发与验证 JWT</h2><p>在应用里实施使用基于 JWT 这种 Token 的身份验证方法，你可以先去找一个签发与验证 JWT 的功能包。无论你的后端应用使用的是什么样的程序语言，系统，或者框架，你应该都可以找到提供类似功能的包。</p>
<p>下面我们在一个 Node.js 项目里，用最简单的方式来演示一下签发还有验证 JWT 的方法。</p>
<h3 id="准备项目"><a href="#准备项目" class="headerlink" title="准备项目"></a>准备项目</h3><p>准备一个简单的 Node.js 项目：</p>
<div class="hljs"><pre><code class="hljs properties"><span class="hljs-attr">cd</span> <span class="hljs-string">~/desktop</span>
<span class="hljs-attr">mkdir</span> <span class="hljs-string">jwt-demo</span>
<span class="hljs-attr">cd</span> <span class="hljs-string">jwt-demo</span>
<span class="hljs-attr">npm</span> <span class="hljs-string">init -y</span></code></pre></div>

<p>安装签发与验证 JWT 的功能包，我用的叫 <a href="https://github.com/auth0/node-jsonwebtoken" target="_blank" rel="noopener"><em>jsonwebtoken</em></a>，在项目里安装一下这个包：</p>
<div class="hljs"><pre><code class="hljs mipsasm">npm <span class="hljs-keyword">install </span><span class="hljs-keyword">jsonwebtoken </span>--save</code></pre></div>

<h3 id="签发-JWT"><a href="#签发-JWT" class="headerlink" title="签发 JWT"></a>签发 JWT</h3><p>在项目里随便添加一个 .js 文件，比如 <em>index.js</em>，在文件里添加下面这些代码：</p>
<div class="hljs"><pre><code class="hljs javascript"><span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jsonwebtoken'</span>)

<span class="hljs-comment">// Token 数据</span>
<span class="hljs-keyword">const</span> payload = &#123;
  name: <span class="hljs-string">'wanghao'</span>,
  admin: <span class="hljs-literal">true</span>
&#125;

<span class="hljs-comment">// 密钥</span>
<span class="hljs-keyword">const</span> secret = <span class="hljs-string">'ILOVENINGHAO'</span>

<span class="hljs-comment">// 签发 Token</span>
<span class="hljs-keyword">const</span> token = jwt.sign(payload, secret, &#123; <span class="hljs-attr">expiresIn</span>: <span class="hljs-string">'1day'</span> &#125;)

<span class="hljs-comment">// 输出签发的 Token</span>
<span class="hljs-built_in">console</span>.log(token)</code></pre></div>

<p>非常简单，就是用了刚刚为项目安装的 jsonwebtoken 里面提供的 jwt.sign 功能，去签发一个 token。这个 sign 方法需要三个参数：</p>
<ol>
<li><em>playload</em>：签发的 token 里面要包含的一些数据。</li>
<li><em>secret</em>：签发 token 用的密钥，在验证 token 的时候同样需要用到这个密钥。</li>
<li><em>options</em>：一些其它的选项。</li>
</ol>
<p>在命令行下面，用 <em>node</em> 命令，执行一下项目里的 <em>index.js</em> 这个文件（<em>node index.js</em>），会输出应用签发的 <em>token</em>：</p>
<div class="hljs"><pre><code class="hljs gcode">eyJhbGciOiJIUzI<span class="hljs-number">1</span><span class="hljs-symbol">NiIsInR5</span>cCI<span class="hljs-number">6</span>IkpX<span class="hljs-attr">VCJ9</span>.eyJuYW<span class="hljs-number">1</span>lIjoid<span class="hljs-number">2</span>FuZ<span class="hljs-number">2</span>hhbyIsImFkbWluIjp<span class="hljs-number">0</span>c<span class="hljs-symbol">nVlLCJpYXQiOjE1</span>MjkwMz<span class="hljs-name">M5</span>MDYsImV<span class="hljs-number">4</span>cCI<span class="hljs-number">6</span>MTUyOTEyMDMw<span class="hljs-symbol">Nn0</span>.DctA<span class="hljs-number">2</span>QlUCr<span class="hljs-name">M6</span>wLWkI<span class="hljs-meta">O78</span>wBV<span class="hljs-symbol">N0</span><span class="hljs-symbol">NLpjoIq4</span>T<span class="hljs-number">5</span>B_<span class="hljs-number">2</span>WJ-PU</code></pre></div>

<p>上面的 Token 内容并没有加密，所以如果用一些 JWT 解码功能，可以看到 Token 里面包含的内容，内容由三个部分组成，像这样：</p>
<div class="hljs"><pre><code class="hljs gcode"><span class="hljs-comment">// header</span>
&#123;
  <span class="hljs-string">"alg"</span>: <span class="hljs-string">"HS256"</span>, 
  <span class="hljs-string">"typ"</span>: <span class="hljs-string">"JWT"</span>
&#125;

<span class="hljs-comment">// payload</span>
&#123;
  <span class="hljs-string">"admin"</span>: true, 
  <span class="hljs-string">"iat"</span>: <span class="hljs-number">1529033906</span>, 
  <span class="hljs-string">"name"</span>: <span class="hljs-string">"wanghao"</span>, 
  <span class="hljs-string">"exp"</span>: <span class="hljs-number">1529120306</span>
&#125;

<span class="hljs-comment">// signature</span>
DctA<span class="hljs-number">2</span>QlUCr<span class="hljs-name">M6</span>wLWkI<span class="hljs-meta">O78</span>wBV<span class="hljs-symbol">N0</span><span class="hljs-symbol">NLpjoIq4</span>T<span class="hljs-number">5</span>B_<span class="hljs-number">2</span>WJ-PU</code></pre></div>

<p>假设用户通过了某种身份验证，你就可以使用上面的签发 Token 的功能为用户签发一个 Token。一般在客户端那里会把它保存在 Cookie 或 LocalStorage 里面。</p>
<p>用户下次向我们的应用请求受保护的资源的时候，可以在请求里带着我们给它签发的这个 Token，后端应用收到请求，检查签名，如果验证通过确定这个 Token 是我们自己签发的，那就可以为用户响应回他需要的资源。</p>
<h3 id="验证-JWT"><a href="#验证-JWT" class="headerlink" title="验证 JWT"></a>验证 JWT</h3><p>验证 JWT 的用效性，确定一下用户的 JWT 是我们自己签发的，首先要得到用户的这个 JWT Token，然后用 <em>jwt.verify</em> 这个方法去做一下验证。这个方法是 Node.js 的 jsonwebtoken 这个包里提供的，在其它的应用框架或者系统里，你可能会找到类似的方法来验证 JWT。</p>
<p>打开项目的 index.js 文件，里面添加几行代码：</p>
<div class="hljs"><pre><code class="hljs sas">// 验证 Token
jwt<span class="hljs-meta">.verify(</span>token, <span class="hljs-string">'bad secret'</span>, (<span class="hljs-meta">error</span>, decoded) =&gt; &#123;
  <span class="hljs-meta">if</span> (<span class="hljs-meta">error</span>) &#123;
    console<span class="hljs-meta">.log(</span><span class="hljs-meta">error</span>.<span class="hljs-meta">message</span>)
    <span class="hljs-meta">return</span>
  &#125;
  console<span class="hljs-meta">.log(</span>decoded)
&#125;)</code></pre></div>

<p>把要验证的 Token 数据，还有签发这个 Token 的时候用的那个密钥告诉 <em>verify</em> 这个方法，在一个回调里面有两个参数，<em>error</em> 表示错误，<em>decoded</em> 是解码之后的 Token 数据。</p>
<p>执行：</p>
<div class="hljs"><pre><code class="hljs crmsh"><span class="hljs-keyword">node</span> <span class="hljs-title">~/desktop</span>/jwt-demo/index.js</code></pre></div>

<p>输出：</p>
<div class="hljs"><pre><code class="hljs css"><span class="hljs-selector-tag">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9</span><span class="hljs-selector-class">.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzQ3MzMsImV4cCI6MTUyOTEyMTEzM30</span><span class="hljs-selector-class">.swXojmu7VimFu3BoIgAxxpmm2J05dvD0HT3yu10vuqU</span>

<span class="hljs-selector-tag">invalid</span> <span class="hljs-selector-tag">signature</span></code></pre></div>

<p>注意输出了一个 invalid signature ，表示 Token 里的签名不对，这是因为我们组长 <em>verify</em> 方法提供的密钥并不是签发 Token 的时候用的那个密钥。这样修改一下：</p>
<div class="hljs"><pre><code class="hljs moonscript">jwt.verify(token, secret, <span class="hljs-function"><span class="hljs-params">(<span class="hljs-built_in">error</span>, decoded)</span> =&gt;</span> &#123; ...</code></pre></div>

<p>再次运行，会输出类似的数据：</p>
<div class="hljs"><pre><code class="hljs yaml"><span class="hljs-string">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzUzODYsImV4cCI6MTUyOTEyMTc4Nn0.mkNrt4TfcfmP22xd3C_GQn8qnUmlB39dKT9SpIBTBGI</span>

<span class="hljs-string">&#123;</span> <span class="hljs-attr">name:</span> <span class="hljs-string">'wanghao'</span><span class="hljs-string">,</span> <span class="hljs-attr">admin:</span> <span class="hljs-literal">true</span><span class="hljs-string">,</span> <span class="hljs-attr">iat:</span> <span class="hljs-number">1529035386</span><span class="hljs-string">,</span> <span class="hljs-attr">exp:</span> <span class="hljs-number">1529121786</span> <span class="hljs-string">&#125;</span></code></pre></div>

<h3 id="RS256-算法"><a href="#RS256-算法" class="headerlink" title="RS256 算法"></a>RS256 算法</h3><p>默认签发还有验证 Token 的时候用的是 HS256 算法，这种算法需要一个密钥（密码）。我们还可以使用 RS256 算法签发与验证 JWT。这种方法可以让我们分离开签发与验证，签发时需要用一个密钥，验证时使用公钥，也就是有公钥的地方只能做验证，但不能签发 JWT。</p>
<p>在项目下面创建一个新的目录，里面可以存储即将生成的密钥与公钥文件。</p>
<div class="hljs"><pre><code class="hljs routeros">cd ~/desktop/jwt-demo
mkdir config
cd config</code></pre></div>

<h4 id="密钥"><a href="#密钥" class="headerlink" title="密钥"></a>密钥</h4><p>先生成一个密钥文件：</p>
<div class="hljs"><pre><code class="hljs angelscript">ssh-keygen -t rsa -b <span class="hljs-number">2048</span> -f <span class="hljs-keyword">private</span>.key</code></pre></div>

<h4 id="公钥"><a href="#公钥" class="headerlink" title="公钥"></a>公钥</h4><p>基于上面生成的密钥，再去创建一个对应的公钥：</p>
<div class="hljs"><pre><code class="hljs processing">openssl rsa -in <span class="hljs-keyword">private</span>.<span class="hljs-built_in">key</span> -pubout -outform PEM -out <span class="hljs-keyword">public</span>.<span class="hljs-built_in">key</span></code></pre></div>

<h3 id="签发-JWT（RS256-算法）"><a href="#签发-JWT（RS256-算法）" class="headerlink" title="签发 JWT（RS256 算法）"></a>签发 JWT（RS256 算法）</h3><p>用 RS256 算法签发 JWT 的时候，需要从文件系统上读取创建的密钥文件里的内容。</p>
<div class="hljs"><pre><code class="hljs javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>)

<span class="hljs-comment">// 获取签发 JWT 时需要用的密钥</span>
<span class="hljs-keyword">const</span> privateKey = fs.readFileSync(<span class="hljs-string">'./config/private.key'</span>)</code></pre></div>

<p>签发仍然使用 jwt.sign 方法，只不过在选项参数里特别说明一下使用的算法是 RS256：</p>
<div class="hljs"><pre><code class="hljs gams"><span class="hljs-comment">// 签发 Token</span>
const tokenRS256 = jwt.<span class="hljs-built-in">sign</span>(payload, privateKey, &#123; algorithm: <span class="hljs-string">'RS256'</span> &#125;)

<span class="hljs-comment">// 输出签发的 Token</span>
console.<span class="hljs-built-in">log</span>(<span class="hljs-string">'RS256 算法：'</span>, tokenRS256)</code></pre></div>

<h2 id=""><a href="#" class="headerlink" title=""></a></h2>
            </article>
            <hr>
            <div>
              <div class="post-metas mb-3">
                
                
              </div>
              
                <p class="note note-warning">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-sa/4.0/deed.zh" target="_blank" rel="nofollow noopener noopener">CC BY-SA 4.0 协议</a> ，转载请注明出处！</p>
              
              
                <div class="post-prevnext row">
                  <div class="post-prev col-6">
                    
                    
                      <a href="/blog/2020/06/26/%E6%A1%86%E6%9E%B6%E4%B8%AD%E5%88%97%E8%A1%A8%E7%BB%84%E4%BB%B6key%E7%9A%84%E4%BD%9C%E7%94%A8/">
                        <i class="iconfont icon-arrowleft"></i>
                        <span class="hidden-mobile">框架中列表组件key的作用</span>
                        <span class="visible-mobile">上一篇</span>
                      </a>
                    
                  </div>
                  <div class="post-next col-6">
                    
                    
                      <a href="/blog/2020/06/26/%E5%89%8D%E7%AB%AF%E4%B8%8E%E6%95%B0%E6%8D%AE%E5%9F%8B%E7%82%B9/">
                        <span class="hidden-mobile">前端与数据埋点</span>
                        <span class="visible-mobile">下一篇</span>
                        <i class="iconfont icon-arrowright"></i>
                      </a>
                    
                  </div>
                </div>
              
            </div>

            
              <!-- Comments -->
              <div class="comments" id="comments">
                
                

              </div>
            
          </div>
        </div>
      </div>
    </div>
    
      <div class="d-none d-lg-block col-lg-2 toc-container" id="toc-ctn">
        <div id="toc">
  <p class="toc-header"><i class="iconfont icon-list"></i>&nbsp;目录</p>
  <div id="tocbot"></div>
</div>

      </div>
    
  </div>
</div>

<!-- Custom -->


    
  </main>

  
    <a id="scroll-top-button" href="#" role="button">
      <i class="iconfont icon-arrowup" aria-hidden="true"></i>
    </a>
  

  
    <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v"
                 for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>
  

  

  

  <footer class="mt-5">
  <div class="text-center py-3">
    <div>
      <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><span>Hexo</span></a>
      <i class="iconfont icon-love"></i>
      <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener">
        <span>Fluid</span></a>
    </div>
    

    

    
  </div>
</footer>

<!-- SCRIPTS -->
<script  src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js" ></script>
<script  src="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/js/bootstrap.min.js" ></script>
<script  src="/blog/js/debouncer.js" ></script>
<script  src="/blog/js/main.js" ></script>

<!-- Plugins -->


  
    <script  src="/blog/js/lazyload.js" ></script>
  



  <script defer src="https://cdn.staticfile.org/clipboard.js/2.0.6/clipboard.min.js" ></script>
  <script  src="/blog/js/clipboard-use.js" ></script>



  <script defer src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" ></script>





  <script  src="https://cdn.staticfile.org/tocbot/4.11.1/tocbot.min.js" ></script>
  <script>
    $(document).ready(function () {
      var boardCtn = $('#board-ctn');
      var boardTop = boardCtn.offset().top;

      tocbot.init({
        tocSelector: '#tocbot',
        contentSelector: 'article.markdown-body',
        headingSelector: 'h1,h2,h3,h4,h5,h6',
        linkClass: 'tocbot-link',
        activeLinkClass: 'tocbot-active-link',
        listClass: 'tocbot-list',
        isCollapsedClass: 'tocbot-is-collapsed',
        collapsibleClass: 'tocbot-is-collapsible',
        collapseDepth: 0,
        scrollSmooth: true,
        headingsOffset: -boardTop
      });
      if ($('.toc-list-item').length > 0) {
        $('#toc').css('visibility', 'visible');
      }
    });
  </script>



  <script  src="https://cdn.staticfile.org/typed.js/2.0.11/typed.min.js" ></script>
  <script>
    var typed = new Typed('#subtitle', {
      strings: [
        '  ',
        "如何实现token加密&nbsp;",
      ],
      cursorChar: "|",
      typeSpeed: 70,
      loop: true,
    });
    typed.stop();
    $(document).ready(function () {
      $(".typed-cursor").addClass("h2");
      typed.start();
    });
  </script>



  <script  src="https://cdn.staticfile.org/anchor-js/4.2.2/anchor.min.js" ></script>
  <script>
    anchors.options = {
      placement: "right",
      visible: "hover",
      
    };
    var el = "h1,h2,h3,h4,h5,h6".split(",");
    var res = [];
    for (item of el) {
      res.push(".markdown-body > " + item)
    }
    anchors.add(res.join(", "))
  </script>



  <script  src="/blog/js/local-search.js" ></script>
  <script>
    var path = "/blog/local-search.xml";
    var inputArea = document.querySelector("#local-search-input");
    inputArea.onclick = function () {
      searchFunc(path, 'local-search-input', 'local-search-result');
      this.onclick = null
    }
  </script>



  <script  src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js" ></script>
  <link  rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css" />

  <script>
    $('#post img:not(.no-zoom img, img[no-zoom]), img[zoom]').each(
      function () {
        var element = document.createElement('a');
        $(element).attr('data-fancybox', 'images');
        $(element).attr('href', $(this).attr('src'));
        $(this).wrap(element);
      }
    );
  </script>







  
  
    <script>
      !function (e, t, a) {
        function r() {
          for (var e = 0; e < s.length; e++) s[e].alpha <= 0 ? (t.body.removeChild(s[e].el), s.splice(e, 1)) : (s[e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x + "px;top:" + s[e].y + "px;opacity:" + s[e].alpha + ";transform:scale(" + s[e].scale + "," + s[e].scale + ") rotate(45deg);background:" + s[e].color + ";z-index:99999");
          requestAnimationFrame(r)
        }

        function n() {
          var t = "function" == typeof e.onclick && e.onclick;
          e.onclick = function (e) {
            t && t(), o(e)
          }
        }

        function o(e) {
          var a = t.createElement("div");
          a.className = "heart", s.push({
            el: a,
            x: e.clientX - 5,
            y: e.clientY - 5,
            scale: 1,
            alpha: 1,
            color: c()
          }), t.body.appendChild(a)
        }

        function i(e) {
          var a = t.createElement("style");
          a.type = "text/css";
          try {
            a.appendChild(t.createTextNode(e))
          } catch (t) {
            a.styleSheet.cssText = e
          }
          t.getElementsByTagName("head")[0].appendChild(a)
        }

        function c() {
          return "rgb(" + ~~(255 * Math.random()) + "," + ~~(255 * Math.random()) + "," + ~~(255 * Math.random()) + ")"
        }

        var s = [];
        e.requestAnimationFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (e) {
          setTimeout(e, 1e3 / 60)
        }, i(".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}"), n(), r()
      }(window, document);
    </script>
  













</body>
</html>
